#!/bin/sh
#
# Copyright (c) 2012, Henrik Hallberg (henrik@k2h.se)
# All rights reserved.
#
# See LICENSE

VERSION=
if [[ -d /run ]]; then
    STATE_DIR=/run/pvpn
else
    STATE_DIR=/var/run/pvpn
fi

usage() {
    echo "Establish VPN through your run of the mill SSH server (with root SSH access)"
    echo ""
    echo "See pvpn(8) for a complete reference, or https://www.github.com/halhen/pvpn"
    echo ""
    echo "USAGE"
    echo "-----"
    echo "  Start tunnel"
    echo "    $ pvpn [option] ... [user@]ssh-host [route] ..."
    echo ""
    echo "    -t TYPE|--type TYPE"
    echo "      Type of tunnel. Valid options are"
    echo "      * ppp (ppp over ssh)"
    echo "      * ssh-3 (OpenSSH Layer 3)"
    echo "    -i IP_PAIR|--ip IP_PAIR"
    echo "      Pair of IP addresses for tunnel"
    echo "      If no -i is given, 10.b.c.1:10.b.c.2, where b and c are random, is used."
    echo "    -s HOST|--first-ssh-hop HOST"
    echo "      IP address or hostname of the first SSH server you connect to. Use if you"
    echo "      connect through reverse tunnels or intermediate servers with ProxyCommand."
    echo "    -D|--inherit-dns"
    echo "      Inherit DNS settings from ssh-host. When disconnecting, the current DNS"
    echo "      settings will be restored."
    echo ""
    echo "  Stop tunnel"
    echo "    $ pvpn --stop [tunnel]"
    echo ""
    echo "    -S TUNNEL|--stop TUNNEL"
    echo "      Stop tunnel with client device TUNNEL and exit. If no TUNNEL is given, stop"
    echo "      all tunnels."
    echo ""
    echo "EXAMPLES"
    echo "--------"
    echo "  Gain access to the 192.168.xxx.yyy network at work"
    echo "    $ pvpn -i 10.10.0.1:10.10.0.2 root@work 192.168.0.0/16"
    echo ""
    echo "  Encrypt all IP traffic, e.g. when on a public wifi, using OpenSSH Layer 3"
    echo "    $ pvpn -t ssh-3 root@secureproxy default"
    echo ""
    echo "  Disconnect tunnel with device ppp0"
    echo "    $ pvpn -S ppp0"
    echo ""
    echo "SEE ALSO"
    echo "--------"
    echo "ssh(1), sshd(8), sshd_config(5), pppd(8), ssh-keygen(1), iptables(8)"
}


do_args() {
    while [[ "$#" -gt 0 ]]; do
        case "$1" in
            -h|--help)
                usage; exit 1;;
            -v|--version)
                die "pvpn v. $VERSION";;
            -d|--debug)
                set -x;;
            -t|--type)
                case "$2" in
                    ppp)
                        tunnel_type=ppp;;
                    ssh-3)
                        tunnel_type=ssh_layer3;;
                    *)
                        die "Unknown tunnel type '$2'";;
                esac
                shift;;
            -i|--ip)
                ip_pair="$2"
                shift;;
            -s|--first-ssh-hop)
                ssh_ip="$(nslookup $2 114.114.114.114 |grep -v 114.114.114.114| grep "Address 1"|cut -d" " -f 3)"
                shift;;
            -D|--inherit-dns)
                inherit_dns=true;;
            -S|--stop)
                disconnect "$2"
                exit 0;;
            -e|--ssh)
                SSH=$2
                shift
                ;;
            -r|--route-file)
                route_file="$2"
                shift
                ;;
            -*)
                die "Unknown argument: $1";;
            *)
                break;;
        esac
        shift
    done

    ssh_host="$1"
    [[ -z "$ssh_host" ]] && die "Missing ssh-host, see 'pvpn --help'"
    shift

    [[ -z "$SSH" ]] && SSH=/usr/bin/ssh

    routes="$@"

    # Fill in defaults
    tunnel_type=${tunnel_type:-ppp}
    inherit_dns="${inherit_dns:-false}"
    ip_pair="${ip_pair:-any:any}"
    b=$((`head -n10 /dev/urandom |strings |grep -oE "[1-9]+"|tr -d "\n"` % 256 ))
    c=$((`head -n10 /dev/urandom |strings |grep -oE "[1-9]+"|tr -d "\n"` % 256 ))
    if [[ "${ip_pair%:*}" == "any" ]]; then
        ip_pair="10.$b.$c.1:${ip_pair#*:}"
    fi
    if [[ "${ip_pair#*:}" == "any" ]]; then
        ip_pair="${ip_pair%:*}:10.$b.$c.2"
    fi
}


connect_ppp() {
    which pppd &>/dev/null || die "pppd(8) missing, install or consider using '-t ssh-3'"

    pppd_output=$(mktemp)
    tempfile=$(mktemp)
    client_device=`ifconfig |grep -e "^pvpn[0-9]"|cut -d' ' -f 1 >$tempfile ; if [ ! -s $tempfile ];then echo pvpn1;exit ; fi; i=50 ; while [ $i -ge 1 ] ;do echo "pvpn$i" ;let i-- ;done | grep -v -f $tempfile | tail -n1 `
    rm -f $tempfile
    pppd ifname $client_device lcp-echo-failure 10 lcp-echo-interval 3 updetach noauth silent nodeflate pty "${SSH} ${SSH_ARGS} -oConnectTimeout=50 -oServerAliveInterval=55 '$ssh_host' pppd nodetach notty noauth" ipparam vpn "$ip_pair" >"$pppd_output" || die "pppd failed (exit code $?)"
    #read client_device server_device <$(awk '/^Connect/ {print $2 " " $4; exit;}' <"$pppd_output")
    rm "$pppd_output"
    #client_device=$(logread |grep "Connect:"|tail -n1|awk '{print $8}')
    #server_device=$(logread |grep "Connect:"|tail -n1|awk '{print $10}')
    stop_cmd="kill $(cat "/var/run/$client_device.pid")"
}


connect_ssh_layer3() {
    device_pair=$(available_devicepair "$ssh_host" "tun")
    echo $device_pair

    client_ip="${ip_pair%:*}"
    server_ip="${ip_pair#*:}"
    client_device="${device_pair%:*}"
    server_device="${device_pair#*:}"
    ${SSH} -TCf ${SSH_ARGS} -oServerAliveInterval=55 \
             -oExitOnForwardFailure=yes \
             -oTunnel=point-to-point \
             -oConnectTimeout=50 \
             -w "${client_device#tun}:${server_device#tun}" "$ssh_host" "\
     ip link set $server_device up; \
     ip addr add $server_ip/32 peer $client_ip dev $server_device;" || \
        die "ssh failed (exit code $?)"
    pid=$(for pid in `ps |grep -v grep|grep ssh |awk '{print $1}'` ;do if [ -f /proc/$pid/cmdline ] ;then if grep -q "ip link set $server_device up" /proc/$pid/cmdline;then echo $pid ; fi; fi;done)
    stop_cmd="kill $pid"


    # FIXME: -oExitOnForwardFailure and -f doesn't seem to play along as
    # nicely as the docs say. Wait a second and fail on the step below instead
    # if we didn't get a tunnel up
    sleep 1

    ip link set $client_device up || \
        die "Failed to set $client_device up"
    ip addr add $client_ip/32 peer $server_ip dev $client_device || \
        die "Failed to set $client_ip on $client_device"
}


disconnect() {
    if [[ -z "$1" ]]; then
        for filename in $(find "$STATE_DIR" -type f -name '[^.]*'); do
            disconnect "$(basename "$filename")"
        done
        return
    fi

    filename="$STATE_DIR/$1"
    source "$filename"
    eval $stop_cmd
    [[ -n "$ssh_ip" ]] && ip route delete "$ssh_ip"
    rm "$filename"
    echo "$1 disconnected"
}


route() {
    gateway="$1"
    shift

    # No routes to add; return
    [[ $# -eq 0 ]] && return 0

    [[ -z "$ssh_ip" ]] && \
        ssh_ip="$(nslookup "${ssh_host#*@}"  114.114.114.114 |grep -v 114.114.114.114| grep "Address 1"|cut -d" " -f 3 )"

    # Add route to ssh host through current gateway
    current_default=$(ip route show 0.0.0.0/0 | head -n1 | cut -d' ' -f3)
    if [[ -z "$ssh_ip" ]]; then
        echo "IP of the first SSH hop not found. Consider using the -s switch."
    else
        echo "Routing $ssh_ip through current default gateway ($current_default)"
        ip route add "$ssh_ip" via "$current_default"
    fi

    while [[ "$#" -gt 0 ]]; do
        route="$1"
        echo "Routing $route via $gateway"
        case $route in
            default|*/0)
                ip route replace default via "$gateway"
                stop_cmd="$stop_cmd;ip route replace default via $current_default"
                ;;
            *)
                ip route add "$route" via "$gateway";;
        esac
        shift
    done
    [ -f "${route_file}" ] &&cat "${route_file}"|sort -u| while read route ;do
        ip route add "$route" via "$gateway"
    done
}


dns() {
    if ! $inherit_dns; then
        return
    fi

    cp /etc/resolv.conf     "$STATE_DIR/.$client_device.pre.resolv.conf"
    stop_cmd="$stop_cmd; mv '$STATE_DIR/.$client_device.pre.resolv.conf' /etc/resolv.conf"

    scp -q "$ssh_host":/etc/resolv.conf /etc/resolv.conf
}

available_devicepair() {
    local ssh_host="$1"
    local device_type="$2"
    local tempfile

    [[ -z "$ssh_host" ]] && die "available_devicepair(): Missing ssh_host"
    case "$device_type" in
        tun|tap) :;;
        "") die "available_devicepair(): Missing device_type";;
        *)  die "available_devicepair(): Bad device_type '$device_type'";;
    esac
    tempfile=$(mktemp)
    cmd_available_tun_device="ifconfig |grep -e \"^tun[0-9]\"|cut -d' ' -f 1 >$tempfile ;if [ ! -s $tempfile ] ;then echo tun50;exit;fi; i=0 ; while [ \$i -le 50 ] ;do echo \"$device_type\$i\" ;let i++ ;done | grep -v -f $tempfile | tail -n1 ;rm -f $tempfile"
    #cmd_available_tun_device="exclude=\"\$(ip tuntap |cut -d: -f1)\" ;if [ \"\$exclude\" != \"\" ]; then  echo $devices | tr ' ' '\n' | grep -v \"\$exclude\" | tail -n1 ;else  echo $devices | tr ' ' '\n' | tail -n1 ;fi "
    client_tun_device="$(eval $cmd_available_tun_device)"
    server_tun_device="$(${SSH} ${SSH_ARGS} "$ssh_host" $cmd_available_tun_device)"

    echo "$client_tun_device:$server_tun_device"
}


write_statefile() {
    filename="$1"
    [[ -z "$filename" ]] && filename="$STATE_DIR/$client_device"
    mkdir -p "$(dirname "$filename")"

    : >"$filename"
    echo "ssh_host=\"$ssh_host\""           >>"$filename"
    echo "ssh_ip=\"$ssh_ip\""               >>"$filename"
    echo "stop_cmd=\"$stop_cmd\""           >>"$filename"
    echo "tunnel_type=\"$tunnel_type\""     >>"$filename"
    echo "inherit_dns=\"$inherit_dns\""     >>"$filename"
    echo "ip_pair=\"$ip_pair\""             >>"$filename"
    echo "client_device=\"$client_device\"" >>"$filename"
    echo "server_device=\"$server_device\"" >>"$filename"
    echo "routes=\"$routes\""               >>"$filename"
}


check_root() {
    [[ $(id -u) -eq 0 ]] || die "Must be root"
}


die() {
    echo "$@" >&2
    exit 1
}

check_root
mkdir -p "$STATE_DIR"
do_args "$@"
connect_$tunnel_type 
route "${ip_pair#*:}" ${routes}
dns
write_statefile
echo "$client_device connected"
